オンライン雑談会の日程調整で 「調整さんの回答期限は今日だよ!!」 をSlackに自動投稿する仕組みをサーバーレスで作りました
定期開催しているオンライン雑談会があります。日時の調整に調整さんが大活躍しています。 おおよその作業フローは下記です。
- 月初に「調整さんを作ってね」のリマインダーがSlackに自動投稿される(以前に作成した仕組み)
- 人間が調整さんを作成する
- 人間がSlackのチャンネルで案内する(URLと締切)
- 人間が締切日に「今日が締切だよ!」と案内する
地味にめんどくさい、かつ、忘れがちな 3.
と4.
をテコ入れしてサーバーレスによる自動化に挑戦しました。
なお、下記で「開催日は今日だよ!」と案内する仕組みも導入しました。
おすすめの方
- AWS SAMを使いたい
- SlackでAppを作りたいい
- SlackでEvents APIを使いたい
- SlackでIncoming Webhookを使いたい
- LambdaでSlackに投稿したい
最初に作ったものをご紹介(完成版)
作業フロー(ユーザ視点)
- 毎月初日に「調整さん作ってね」がチャンネルに自動投稿される(以前に作成)
- 人間が調整さんを作成し、Slackのワークフローで登録する(URLと締切日)
- 締切日の10時に「今日が締切だよ」がチャンネルに自動投稿される
1. 毎月初日に「調整さん作ってね」がチャンネルに自動投稿される
コレ自体は以前に作成した仕組みです。
2. 人間が調整さんを作成し、Slackのワークフローで登録する(URLと締切日)
調整さんを作成したあと、Slackのワークフローを人間が起動し、「調整さんのURL」と「回答期限」を入力します。 このワークフローはチャンネルに投稿してお知らせします。
3. 締切日の10時に「今日が締切だよ」がチャンネルに自動投稿される
全体概要
SlackのEvents APIを利用してチャンネルのメッセージを監視し、ワークフロー経由で投稿した「調整さんのURLと期限」をDynamoDBへ保存します。 その後、1日1回の頻度でDynamoDBを確認し、期限が今日の項目があればSlackにその旨を投稿しています。
DynamoDBのハッシュキーは年月日のunixtime(0時0分0秒)を使っています。これは処理を楽にするため、かつ、同じ日に複数の調整さんを登録しないためです。 また、DynamoDBのデータ削除はTTL機能を使って自動削除させています。最大でも48時間後の削除になりえますが、少ないサンプル数での実績としてほぼすぐ消えたこと、および、2日連続で通知されることを許容します。
環境&前提
項目 | バージョン |
---|---|
macOS | Mojave 10.14.6 |
Python | 3.7 |
SAM CLI | version 0.37.0 |
下記の続きで作成していきます。
目次(ここから下)
- 期日をDynamoDBに保存するAPI(Lambda)を作成する(verify用)
- Slackのアプリを作成する
- Slackアプリをワークスペースに追加する
- Slackのチャンネルにアプリを追加する
- Slackのワークフローを作成する
- 期日をDynamoDBに保存するAPI(Lambda)を作成する
- 締め切りを告知するLambdaを作成する
- さいごに
期日をDynamoDBに保存するAPI(Lambda)を作成する(verify用)
チャンネルのメッセージを受け取るAPIを作成しますが、「そのAPIは有効なの?」を確認する必要があるため、まずはSlackの仕様に基づいた応答を返すAPIを作成します。
AWS SAMテンプレート
前回の内容も含んでいます。また、この時点で下記も追加しています。
- DynamoDBの定義
- LambdaにDynamoDBFullAccessを付与
AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: Chouseisan-reminder Parameters: ChouseisanNotifySlackUrl: Type : AWS::SSM::Parameter::Value<String> Resources: ChouseisanReminderFunction: Type: AWS::Serverless::Function Properties: FunctionName: chouseisan-reminder-function CodeUri: src/notify_1st_message/ Handler: app.lambda_handler Runtime: python3.7 Timeout: 10 Environment: Variables: TZ: Asia/Tokyo INCOMMING_WEBHOOK_URL: !Ref ChouseisanNotifySlackUrl Events: NotifySlack: Type: Schedule Properties: Schedule: cron(0 0 1-7 * ? *) # 日本時間で毎月1-7日のAM9時 ChouseisanReminderFunctionLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub "/aws/lambda/${ChouseisanReminderFunction}" ChouseisanSaveDeadlineFunction: Type: AWS::Serverless::Function Properties: FunctionName: chouseisan-save-deadline-function CodeUri: src/save_deadline/ Handler: app.lambda_handler Runtime: python3.7 Timeout: 10 Environment: Variables: TZ: Asia/Tokyo REMINDER_TABLE_NAME: !Ref ChouseisanReminderTable Policies: - arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess Events: WorkflowApi: Type: Api Properties: Path: /reminder Method: post ChouseisanSaveDeadlineFunctionLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub "/aws/lambda/${ChouseisanSaveDeadlineFunction}" ChouseisanReminderTable: Type: AWS::DynamoDB::Table Properties: TableName: chouseisan-reminder-table KeySchema: - AttributeName: deadline KeyType: HASH AttributeDefinitions: - AttributeName: deadline AttributeType: N TimeToLiveSpecification: AttributeName: expiration Enabled: true BillingMode: PAY_PER_REQUEST Outputs: SaveDeadlineApi: Description: "save deadline api" Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/reminder/"
Lambdaコード
src/save_deadline/app.py
を新規作成します。
Events APIのドキュメントに従って、受け取ったパラメータのchallenge
を返しています。
import json def lambda_handler(event, context): body = json.loads(event['body']) return { 'statusCode': 200, 'body': json.dumps( {'challenge': body['challenge']}, ), }
ビルド&デプロイ
sam build
sam package \ --output-template-file packaged.yaml \ --s3-bucket cm-fujii.genki-chouseisan-reminder-deploy-bucket
sam deploy \ --template-file packaged.yaml \ --stack-name Chouseisan-Reminder-Stack \ --capabilities CAPABILITY_NAMED_IAM \ --no-fail-on-empty-changeset \ --parameter-overrides ChouseisanNotifySlackUrl=/Slack/INCOMING_WEBHOOK_URL/channel_name/choseisan_reminder
Slackのアプリを作成する
下記を持つアプリを作成します。
- Event Subscriptions (Events API)
- Incomming Webhook
まずはアプリを新規作成する
SlackのBasic app setupを開き、アプリを新規作成します。Create a new slack app
を選択します。
続いてCreate New App
を選択します。
「App Name」を適当に記入して、Create App
を選択します。
Event Subscriptionsを設定する
Event Subscriptions
を選択します。
有効化したあと、Requests URL
を入力します。Verified
になればOKです!
続いてSubscribe to bot events
にmessage.channels
を追加し、Save
します。
Incomming Webhookを設定する
Incoming Webhooks
を選択してOn
にします。
続いてAdd New Webhook to Workspace
を選択して追加します。
Slackアプリをワークスペースに追加する
OAuth & Permissions
を選択し、Install App to Workspace
を選択します。
Slackのチャンネルにアプリを追加する
任意のチャンネルの詳細メニューから追加できます。
Slackのワークフローを作成する
ワークフローを新規作成
Slackの左上を選択し、ワークフロービルダーを起動します。
適当に名前をつけます。
人間が起動するためショートカット
を選択します。
適当に入力します。
ワークフローのステップを追加(フォーム入力)
ステップを追加
を選択します。
フォームを作成する
を選択します。
調整さんのURLと回答期限を入力するフォームを作成します。
それぞれは下記です。
ワークフローのステップを追加(メッセージ送信)
さらにステップを追加
します。
今度はメッセージを送信
を選択します。
そして保存しましょう。
ワークフローを公開する
右上の公開する
ボタンを選択すればOKです!
JSONメモ
このワークフローを実行したとき、Lambdaには次のJSONが来ました。
{ "token": "xxxxx", "team_id": "xxxxx", "api_app_id": "xxxxx", "event": { "type": "message", "subtype": "bot_message", "text": "調整さんに記入をお願いします!\n期限は *2020/04/06* です!\n<https://chouseisan.com/s?h=xxxxx>", "ts": "1586171318.003000", "username": "reminder_misc_join_201901_workflow", "bot_id": "xxxxx", "channel": "xxxxx", "event_ts": "1586171318.003000", "channel_type": "channel" }, "type": "event_callback", "event_id": "xxxxx", "event_time": 1586171318, "authed_users": [ "xxxxx" ] }
期日をDynamoDBに保存するAPI(Lambda)を作成する
AWS SAMにはDynamoDBの定義を追加済みなのでLambdaコードのみを修正します。
Lambdaコードを変更
Lambdaコード(src/save_deadline/app.py
)を下記にします。
import boto3 import json import logging import os import re from datetime import datetime logger = logging.getLogger() logger.setLevel(logging.INFO) dynamodb = boto3.resource('dynamodb') def lambda_handler(event, context): main(event) return { 'statusCode': 200 } def main(event): logger.info(json.dumps(event)) body = json.loads(event['body']) logger.info(json.dumps(body)) if 'username' not in body['event']: logger.info('No username.') return if 'reminder_misc_join_201901_workflow' != body['event']['username']: logger.info('No workflow message.') return deadline = parse_timestamp(body['event']['text']) url = parse_url(body['event']['text']) logger.info(f'deadline: {deadline}, url: {url}') # 当日11時をDynamoDBのTTL期限とする expiration = deadline + 60*60*11 put_item(deadline, url, expiration) def parse_timestamp(text): pattern = r'.+\n期限は \*(\d{4}/\d{1,2}/\d{1,2})\* です!' res = re.match(pattern, text) if res: # 0時のunixtimeを返す return int(datetime.strptime(res.group(1), '%Y/%m/%d').timestamp()) raise ValueError def parse_url(text): pattern = r'.+\n.+\n<(.+)>' res = re.match(pattern, text) if res: return res.group(1) raise ValueError def put_item(deadline, url, expiration): table_name = os.environ['REMINDER_TABLE_NAME'] table = dynamodb.Table(table_name) res = table.put_item(Item={ 'deadline': deadline, 'expiration': expiration, 'url': url }) logger.info(res)
ビルド&デプロイ
sam build
sam package \ --output-template-file packaged.yaml \ --s3-bucket cm-fujii.genki-chouseisan-reminder-deploy-bucket
sam deploy \ --template-file packaged.yaml \ --stack-name Chouseisan-Reminder-Stack \ --capabilities CAPABILITY_NAMED_IAM \ --no-fail-on-empty-changeset \ --parameter-overrides ChouseisanNotifySlackUrl=/Slack/INCOMING_WEBHOOK_URL/channel_name/choseisan_reminder
動作確認(簡易)
Slackのワークフローを実行します。
そうすると、DynamoDBにデータが保存されました!
締め切りを告知するLambdaを作成する
1日1回起動し、今日が締め切りなら告知するLambdaを作成していきます。
AWS SAM(全部)
AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: Chouseisan-reminder Parameters: ChouseisanNotifySlackUrl: Type : AWS::SSM::Parameter::Value<String> Resources: ChouseisanReminderFunction: Type: AWS::Serverless::Function Properties: FunctionName: chouseisan-reminder-function CodeUri: src/notify_1st_message/ Handler: app.lambda_handler Runtime: python3.7 Timeout: 10 Environment: Variables: TZ: Asia/Tokyo INCOMMING_WEBHOOK_URL: !Ref ChouseisanNotifySlackUrl Events: NotifySlack: Type: Schedule Properties: Schedule: cron(0 0 1-7 * ? *) # 日本時間で毎月1-7日のAM9時 ChouseisanReminderFunctionLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub "/aws/lambda/${ChouseisanReminderFunction}" ChouseisanSaveDeadlineFunction: Type: AWS::Serverless::Function Properties: FunctionName: chouseisan-save-deadline-function CodeUri: src/save_deadline/ Handler: app.lambda_handler Runtime: python3.7 Timeout: 10 Environment: Variables: TZ: Asia/Tokyo REMINDER_TABLE_NAME: !Ref ChouseisanReminderTable Policies: - arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess Events: WorkflowApi: Type: Api Properties: Path: /reminder Method: post ChouseisanSaveDeadlineFunctionLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub "/aws/lambda/${ChouseisanSaveDeadlineFunction}" ChouseisanReminderTable: Type: AWS::DynamoDB::Table Properties: TableName: chouseisan-reminder-table KeySchema: - AttributeName: deadline KeyType: HASH AttributeDefinitions: - AttributeName: deadline AttributeType: N TimeToLiveSpecification: AttributeName: expiration Enabled: true BillingMode: PAY_PER_REQUEST ChouseisanNotifyDeadlineFunction: Type: AWS::Serverless::Function Properties: FunctionName: chouseisan-notify-deadline-function CodeUri: src/notify_deadline_message/ Handler: app.lambda_handler Runtime: python3.7 Timeout: 10 Environment: Variables: TZ: Asia/Tokyo REMINDER_TABLE_NAME: !Ref ChouseisanReminderTable INCOMMING_WEBHOOK_URL: !Ref ChouseisanNotifySlackUrl Policies: - arn:aws:iam::aws:policy/AmazonDynamoDBReadOnlyAccess Events: NotifySlack: Type: Schedule Properties: Schedule: cron(0 1 * * ? *) # 日本時間AM10時に毎日通知する ChouseisanNotifyDeadlineFunctionLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub "/aws/lambda/${ChouseisanNotifyDeadlineFunction}" Outputs: SaveDeadlineApi: Description: "save deadline api" Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/reminder/"
Lambdaコード
import boto3 import json import logging import os import requests from botocore.exceptions import ClientError from datetime import date, datetime logger = logging.getLogger() logger.setLevel(logging.INFO) INCOMMING_WEBHOOK_URL = os.environ['INCOMMING_WEBHOOK_URL'] dynamodb = boto3.resource('dynamodb') def lambda_handler(event, context): today = get_today() logger.info(f'today: {today}') remind_data = get_remind_data(today) logger.info(f'get_remind_data(): {remind_data}') if remind_data is None: return # Slackに通知する post_slack(remind_data) def get_today(): today = date.today() # 今日の0時0分0秒のunixtimeを返す return int(datetime(today.year, today.month, today.day).timestamp()) def get_remind_data(deadline): table_name = os.environ['REMINDER_TABLE_NAME'] table = dynamodb.Table(table_name) try: res = table.get_item(Key={ 'deadline': deadline } ) except ClientError as e: logger.error(e.response['Error']['Message']) return None else: return res.get('Item', None) def post_slack(remind_data): # https://api.slack.com/incoming-webhooks # https://api.slack.com/docs/message-formatting # https://api.slack.com/docs/messages/builder payload = { # https://www.webfx.com/tools/emoji-cheat-sheet/ 'icon_emoji': ':bangbang:', 'text': '<!here> 今日が締切です!! 記入お願いします!\n', 'attachments': [ { 'text': remind_data['url'] } ] } url = f'https://{INCOMMING_WEBHOOK_URL}' # http://requests-docs-ja.readthedocs.io/en/latest/user/quickstart/ try: response = requests.post(url, data=json.dumps(payload)) except requests.exceptions.RequestException as e: logger.error(e) else: logger.info(response.status_code)
ビルド&デプロイ
sam build
sam package \ --output-template-file packaged.yaml \ --s3-bucket cm-fujii.genki-chouseisan-reminder-deploy-bucket
sam deploy \ --template-file packaged.yaml \ --stack-name Chouseisan-Reminder-Stack \ --capabilities CAPABILITY_NAMED_IAM \ --no-fail-on-empty-changeset \ --parameter-overrides ChouseisanNotifySlackUrl=/Slack/INCOMING_WEBHOOK_URL/channel_name/choseisan_reminder
動作確認(期限の通知)
期日のAM10時に通知が来ました!!
さいごに
Slackのワークフローとサーバーレスな組み合わせで、期日を自動通知する仕組みを作ってみました。 少しでも便利になったはずです!!